﻿using ADEVSO.Util;
using SmartParking.Data;
using SmartParking.Data.Entities;
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Entity.Core.EntityClient;
using System.Linq;
using System.Transactions;
using System.Web;

namespace SmartParking.Bussiness
{
    public static class ReservationsManager
    {
        private const string OrderTableName = "Orders";
        private const string OrderDetailsTableName = "OrderDetails";

        /// <summary>
        /// Return all orders from the order requested
        /// </summary>
        private static List<Order> GetOrders(Func<Order, bool> conditions = null)
        {
            using (var context = new SmartParkingContext())
            {
                return context.Orders.Where(conditions).ToList();
            }
        }

        /// <summary>
        /// Return all orders from the user requested
        /// </summary>
        public static List<Order> GetOrders(long userId = -1)
        {
            var conditions = new Func<Order, bool>(item => !item.Deleted);
            if (userId != -1) conditions += item => item.UserID == userId;
            return GetOrders(conditions);
        }

        /// <summary>
        /// Return all reservations from the order requested
        /// </summary>
        public static List<OrderDetail> GetOrderReservations(string orderId)
        {
            using (var context = new SmartParkingContext())
            {
                return context.OrderDetails.Where(item => item.OrderID == orderId).ToList();
            }
        }

        /// <summary>
        /// Return global reservations collection
        /// </summary>
        public static Dictionary<String, List<OrderDetail>> GetGlobalReservations(HttpApplicationState application)
        {
            var globalReservations = application["Reservations"] as Dictionary<String, List<OrderDetail>>;
            if (globalReservations == null)
            {
                globalReservations = new Dictionary<String, List<OrderDetail>>();
                application["Reservations"] = globalReservations;
            }
            return globalReservations;
        }

        /// <summary>
        /// Save global reservations collection
        /// </summary>
        private static void SaveGlobalReservations(HttpApplicationState application, Dictionary<String, List<OrderDetail>> reservations)
        {
            application["Reservations"] = reservations;
        }

        /// <summary>
        /// Return all reservations in all current sessions
        /// </summary>
        public static List<OrderDetail> GetLiveReservations(HttpApplicationState application)
        {
            var reservations = new List<OrderDetail>();
            var globalReservations = GetGlobalReservations(application);
            foreach (string sessionId in globalReservations.Keys)
                reservations.AddRange(globalReservations[sessionId]);
            return reservations;
        }

        /// <summary>
        /// Return all parking zone reservations in all current sessions
        /// </summary>
        public static List<OrderDetail> GetLiveReservations(HttpApplicationState application, int parkingZoneId, DateTime startDate, DateTime? endDate = null)
        {
            if (!endDate.HasValue) endDate = startDate;
            var reservations = new List<OrderDetail>();
            var globalReservations = GetGlobalReservations(application);
            foreach (string sessionId in globalReservations.Keys)
            {
                reservations.AddRange(globalReservations[sessionId].Where(item => item.ParkingZoneID == parkingZoneId && startDate <= item.ReservationStartDateTime.Value && item.ReservationStartDateTime.Value < endDate).ToList());
            }
            var processingReservations = GetProcessingReservations(item => item.ParkingZoneID == parkingZoneId && startDate <= item.ReservationStartDateTime.Value && item.ReservationStartDateTime.Value < endDate);
            reservations.AddRange(processingReservations.ToLiveReservations());
            return reservations;
        }

        /// <summary>
        /// Return all reservations in the session requested
        /// </summary>
        public static List<OrderDetail> GetLiveReservations(HttpApplicationState application, HttpSessionStateBase session)
        {
            var reservations = new List<OrderDetail>();
            var globalReservations = GetGlobalReservations(application);
            if (!globalReservations.ContainsKey(session.SessionID))
            {
                globalReservations.Add(session.SessionID, reservations);
                SaveGlobalReservations(application, globalReservations);
            }
            reservations = globalReservations[session.SessionID];
            return reservations;
        }

        /// <summary>
        /// Remove reservation from live reservations
        /// </summary>
        public static void RemoveLiveReservations(HttpApplicationState application, string sessionId, int reservationId)
        {
            var globalLiveReservations = GetGlobalReservations(application);
            var reservations = globalLiveReservations[sessionId];
            reservations.RemoveAt(reservationId);
            globalLiveReservations[sessionId] = reservations;
            SaveGlobalReservations(application, globalLiveReservations);
        }

        /// <summary>
        /// Return parking zone reservations from database
        /// </summary>
        private static List<OrderDetail> GetParkingZoneReservations(int parkingZoneId, DateTime startDate, DateTime? endDate = null)
        {
            if (!endDate.HasValue) endDate = startDate;
            using (var context = new SmartParkingContext())
            {
                return context.OrderDetails.Where(item => !item.Deleted && item.ParkingZoneID == parkingZoneId &&
                    item.Order.IsReservation && startDate <= item.ReservationStartDateTime && item.ReservationEndDateTime <= endDate).ToList();
            }
        }

        /// <summary>
        /// Return all parking zone reservations
        /// </summary>
        public static List<OrderDetail> GetAllParkingZoneReservations(HttpApplicationState application, int parkingZoneId, DateTime startDate)
        {
            // Reservations from database
            var reservations = GetParkingZoneReservations(parkingZoneId, startDate);

            // Live reservations from current sessions
            reservations.AddRange(GetLiveReservations(application, parkingZoneId, startDate));

            return reservations;
        }

        /// <summary>
        /// Create reservation
        /// </summary>
        private static OrderDetail CreateReservation(int parkingId, string date, string dateFormat, string startHour, string endHour, string carPlate)
        {
            var reservation = new OrderDetail {ParkingID = parkingId};
            var dateFormatted = DateTime.ParseExact(date, dateFormat, null);
            var hour = startHour.ToHour();
            var time = new TimeSpan(hour.GetHour(), hour.GetMinutes(), 0);
            reservation.StartDateTime = dateFormatted.Add(time);
            hour = endHour.ToHour();
            time = new TimeSpan(hour.GetHour(), hour.GetMinutes(), 0);
            reservation.EndDateTime = dateFormatted.Add(time);
            reservation.CarPlate = carPlate;
            return reservation;
        }

        /// <summary>
        /// Verify if a reservation would be done
        /// </summary>
        public static bool VerifyReservation(HttpApplicationState application, HttpSessionStateBase session, OrderDetail reservation, bool isReservation)
        {
            var reservations = GetParkingZoneReservations(reservation.ParkingZoneID, reservation.ReservationStartDateTime.Value, reservation.ReservationEndDateTime.Value);
            var filterReservations =
                from carReservation in reservations
                where reservation.Order.IsReservation == carReservation.Order.IsReservation
                select carReservation;

            var liveReservations = GetLiveReservations(application, reservation.ParkingZoneID, reservation.ReservationStartDateTime.Value, reservation.ReservationEndDateTime.Value);
            var reservationsCount = isReservation ? (filterReservations.Count() + liveReservations.Count) : reservations.Count;

            var parkingZone = ParkingManager.GetParkingZone(reservation.ParkingZoneID);

            // Set parking zone capacity
            int parkingZoneCapacity = isReservation ? parkingZone.SpacesToReserve : parkingZone.RegularSpaces;

            Console.WriteLine("Parking Zone: {0} / Capacity {1} / Reservation Count {2}", parkingZone.Name, parkingZoneCapacity, reservationsCount);

            if (reservationsCount > parkingZoneCapacity)
                throw new SmartParkingException("There are more parking reservations than allowed.");
            if (reservationsCount == parkingZoneCapacity)
                throw new SmartParkingException("There are not spaces available in this parking at this time lapsed.");
            if (reservation.ReservationStartDateTime >= reservation.ReservationEndDateTime)
                throw new SmartParkingException("The End date time must be greater than the Start date time.");
            if (reservation.ReservationStartDateTime.Value.TimeOfDay < parkingZone.OpenHour.ToHour().ToDateTime().TimeOfDay || reservation.ReservationEndDateTime.Value.TimeOfDay > parkingZone.CloseHour.ToHour().ToDateTime().TimeOfDay)
                throw new SmartParkingException("The selected parking is closed during the time period selected.");
            
            var hasReservation = false;
            var carLiveReservations = ValidateCarReservation(reservation, liveReservations);
            if (carLiveReservations.Any())
                hasReservation = true;
            else
            {
                var carReservations = ValidateCarReservation(reservation, reservations);
                if (carReservations.Any()) hasReservation = true;
            }
            if (hasReservation)
                throw new SmartParkingException("The current car already has a space reserved at this time.");
            
            return true;
        }

        private static IEnumerable<OrderDetail> ValidateCarReservation(OrderDetail reservation, List<OrderDetail> reservations)
        {
            var carReservations = reservations.Where(item => item.CarPlate == reservation.CarPlate);

            //var carReservationsInterfering = carReservations.Except(item =>
            //    // Reservation ending before existing reservation starts
            //    (reservation.ReservationEndDateTime <= item.ReservationStartDateTime
            //    ||
            //    // Reservation starting after existing reservation ends
            //    reservation.ReservationStartDateTime >= item.ReservationEndDateTime));

            var carReservationsNotInterfering = carReservations.Where(item =>
                // Reservation starting/ending before existing reservation starts
                (reservation.ReservationEndDateTime <= item.ReservationStartDateTime)
                ||
                // Reservation starting after existing reservation ends
                (reservation.ReservationStartDateTime >= item.ReservationEndDateTime));
            var carReservationsInterfering = carReservations.Except(carReservationsNotInterfering);

            return carReservationsInterfering;
        }

        /// <summary>
        /// Add live reservation in the session requested
        /// </summary>
        public static void AddLiveReservations(HttpApplicationState application, string sessionId, List<OrderDetail> reservations)
        {
            var globalReservations = GetGlobalReservations(application);
            globalReservations.Add(sessionId, reservations);
            SaveGlobalReservations(application, globalReservations);
        }

        /// <summary>
        /// Add live reservation in the session requested
        /// </summary>
        public static bool AddLiveReservation(HttpApplicationState application, HttpSessionStateBase session, Int32 managerId, OrderDetail reservation)
        {
            var result = false;

            if (reservation.ReservationStartDateTime >= reservation.ReservationEndDateTime)
                throw new SmartParkingException("EndDateTime must be greater than StartDateTime");
            if (DateTime.Now > reservation.ReservationStartDateTime)
                throw new SmartParkingException("StartDateTime must be greater than now");
            
            var parkingZoneFare = ParkingManager.GetParkingZoneWebFare(reservation.ParkingZoneID);
            var factor = Convert.ToDecimal((reservation.ReservationEndDateTime.Value - reservation.ReservationStartDateTime.Value).TotalMinutes);
            factor = parkingZoneFare.ParkingZone.AllowHalfHour ? factor / 30 / 2 : factor / 60;

            reservation.Amount = parkingZoneFare.PriceHour * factor;
            reservation.Commission = parkingZoneFare.ParkingZone.WebCommisionAmount * factor;

            var globalReservations = GetGlobalReservations(application);
            if (VerifyReservation(application, session, reservation, true))
            {
                reservation.ParkingZoneID = parkingZoneFare.ParkingZoneID;
                if (globalReservations.ContainsKey(session.SessionID))
                    globalReservations[session.SessionID].Add(reservation);
                else globalReservations.Add(session.SessionID, new List<OrderDetail>() { reservation });
                result = true;
            }
            application["Reservations"] = globalReservations;
            return result;
        }

        /// <summary>
        /// Remove all live reservations
        /// </summary>
        public static void RemoveLiveReservations(HttpApplicationState application)
        {
            GetGlobalReservations(application).Clear();
        }

        /// <summary>
        /// Remove all live reservations in the session requested
        /// </summary>
        public static bool RemoveLiveReservations(HttpApplicationState application, string sessionId)
        {
            bool result = GetGlobalReservations(application).Remove(sessionId);
            return result;
        }

        /// <summary>
        /// Convert a live reservation collection to a processing reservation collection
        /// </summary>
        public static List<ProcessingReservation> ToProcessingReservations(this IEnumerable<OrderDetail> reservations, string sessionId)
        {
            var result = new List<ProcessingReservation>();
            reservations.ToList().ForEach(item => result.Add(item.ToProcessingReservation(sessionId)));
            return result;
        }

        /// <summary>
        /// Convert a live reservation to a processing reservation
        /// </summary>
        public static ProcessingReservation ToProcessingReservation(this OrderDetail reservation, string sessionId)
        {
            var result = new ProcessingReservation()
                {
                    SessionID = sessionId,
                    ParkingID = reservation.ParkingID,
                    ParkingZoneID = reservation.ParkingZoneID,
                    CarPlate = reservation.CarPlate,
                    StartDateTime = reservation.StartDateTime,
                    EndDateTime = reservation.EndDateTime,
                    ReservationStartDateTime = reservation.ReservationStartDateTime,
                    ReservationEndDateTime = reservation.ReservationEndDateTime,
                    Amount = reservation.Amount,
                    Commission = reservation.Commission
                };
            return result;
        }        
        
        /// <summary>
        /// Save processing reservations of the session requested
        /// </summary>
        private static void SaveProcessingReservations(string sessionId, List<OrderDetail> reservations)
        {
            try
            {
                using (var context = new SmartParkingContext())
                {
                    var processingReservations = new List<ProcessingReservation>();
                    reservations.ForEach(item => processingReservations.Add(item.ToProcessingReservation(sessionId)));
                    processingReservations.ForEach(item => context.ProcessingReservations.Add(item));
                    context.SaveChanges();
                }
            }
            catch (Exception ex)
            {
                throw ex;
            }            
        }

        /// <summary>
        /// Return processing reservations
        /// </summary>
        public static List<ProcessingReservation> GetProcessingReservations(Func<ProcessingReservation, bool> conditions = null)
        {
            using (var context = new SmartParkingContext())
            {
                return conditions == null ? context.ProcessingReservations.ToList() : context.ProcessingReservations.Where(conditions).ToList();
            }
        }

        /// <summary>
        /// Return processing reservations
        /// </summary>
        public static Dictionary<string, List<ProcessingReservation>> GetGroupedProcessingReservations(Func<ProcessingReservation, bool> conditions = null)
        {
            var reservations = GetProcessingReservations(conditions).OrderBy(item => item.SessionID);
            var result = new Dictionary<string, List<ProcessingReservation>>();
            foreach (var reservation in reservations)
            {
                if (!result.ContainsKey(reservation.SessionID))
                    result.Add(reservation.SessionID, new List<ProcessingReservation>());
                result[reservation.SessionID].Add(reservation);
            }
            return result;
        }

        /// <summary>
        /// Return all reservations of the session requested
        /// </summary>
        public static IEnumerable<ProcessingReservation> GetProcessingReservations(string sessionId)
        {
            var reservations = ReservationsManager.GetProcessingReservations(item => item.SessionID == sessionId);
            //LogManager.Debug(1, "127.0.0.1", String.Format("[GetProcessingReservations] Session: {0} Reservations Count: {1}", sessionId, reservations.Count()));
            return reservations;
        }

        /// <summary>
        /// Remove all reservations in the session requested
        /// </summary>
        public static void RemoveProcessingReservations(Func<ProcessingReservation, bool> conditions = null)
        {
            using (var context = new SmartParkingContext())
            {
                var reservations = conditions == null ? context.ProcessingReservations.ToList() : context.ProcessingReservations.Where(conditions).ToList();
                reservations.ForEach(item => context.ProcessingReservations.Remove(item));
                context.SaveChanges();
            }
        }

        /// <summary>
        /// Remove all reservations in the session requested
        /// </summary>
        public static void RemoveProcessingReservations(string sessionId)
        {
            RemoveProcessingReservations(item => item.SessionID == sessionId);
        }

        /// <summary>
        /// Remove the reservation requested
        /// </summary>
        public static void RemoveProcessingReservations(int id)
        {
            RemoveProcessingReservations(item => item.ProcessingReservationsID == id);
        }

        /// <summary>
        /// Process live reservations to PayPal
        /// </summary>
        public static void ProcessLiveReservations(HttpApplicationState application, HttpSessionStateBase session)
        {
            // Move reservations from live to processing collection
            var reservations = GetLiveReservations(application, session);
            SaveProcessingReservations(session.SessionID, reservations);
            RemoveLiveReservations(application, session.SessionID);
        }

        /// <summary>
        /// Cancel processing reservations from PayPal
        /// </summary>
        public static void CancelProcessingReservations(HttpApplicationState application, string sessionId)
        {
            // Move reservations from processing to live collection
            var reservations = GetProcessingReservations(sessionId).ToLiveReservations();
            RemoveProcessingReservations(sessionId);
            AddLiveReservations(application, sessionId, reservations);
        }

        /// <summary>
        /// Convert a processing reservation collection to a live reservation collection
        /// </summary>
        public static List<OrderDetail> ToLiveReservations(this IEnumerable<ProcessingReservation> reservations)
        {
            var result = new List<OrderDetail>();
            reservations.ToList().ForEach(item => result.Add(item.ToLiveReservation()));
            return result;
        }

        /// <summary>
        /// Convert a processing reservation to live reservation
        /// </summary>
        public static OrderDetail ToLiveReservation(this ProcessingReservation reservation)
        {
            var result = new OrderDetail()
            {
                ParkingID = reservation.ParkingID,
                ParkingZoneID = reservation.ParkingZoneID,
                CarPlate = reservation.CarPlate,
                StartDateTime = reservation.StartDateTime,
                EndDateTime = reservation.EndDateTime,
                ReservationStartDateTime = reservation.ReservationStartDateTime,
                ReservationEndDateTime = reservation.ReservationEndDateTime,
                Amount = reservation.Amount,
                Commission = reservation.Commission
            };
            return result;
        }
        
        /// <summary>
        /// Save PayPal web order on database
        /// </summary>
        public static void SavePayPalWebOrder(HttpApplicationState application, PayPalManager.PayPalResponse payPalResponse, int managerId)
        {
            var sessionId = payPalResponse.Info["invoice"];
            var customInfo = payPalResponse.Info["custom"].Split(';');  // UserId;IPAddress
            var userId = customInfo.Length > 0 ? Convert.ToInt64(customInfo[0]) : 0;
            var ipAddress = customInfo.Length > 1 ? customInfo[1] : "";
            var reservations = GetProcessingReservations(sessionId).ToLiveReservations();
            if (reservations.Any())
            {
                LogManager.Debug(managerId, ipAddress, String.Format("[SavePayPalWebOrder] Reservations Count: {0} Session ID: {1}", reservations.Count(), sessionId));
                if (SaveWebOrder(managerId, ipAddress, userId, reservations, PaymentType.PayPal, payPalResponse.Info["txn_id"], Convert.ToDecimal(payPalResponse.Info["mc_gross"]), Convert.ToDecimal(payPalResponse.Info["mc_fee"])))
                    RemoveProcessingReservations(sessionId);
            }
            else
            {
                LogManager.Warning(managerId, ipAddress, String.Format("[SavePayPalWebOrder] Reservations NOT found for Session ID: {0}", sessionId));
            }
        }

        /// <summary>
        /// Save web order on database
        /// </summary>
        private static bool SaveWebOrder(int managerId, string ipAddress, long userId, List<OrderDetail> reservations, PaymentType paymentType, string paymentAuthorization, decimal paymentAmount, decimal paymentFee)
        {
            try
            {
                //var options = new TransactionOptions()
                //    {
                //        IsolationLevel = IsolationLevel.ReadCommitted,
                //        Timeout = TimeSpan.FromMinutes(1)
                //    };
                //LogManager.Debug(managerID, ipAddress, "[SaveWebOrder] Before Transaction");
                using (var connection = new EntityConnection("name=SmartParkingContext"))
                {
                    if (connection.State == ConnectionState.Closed)
                    {
                        connection.Open();
                    }
                    //LogManager.Debug(managerID, ipAddress, "[SaveWebOrder] Connection Opened");
                    using (var context = new SmartParkingContext(connection))
                    {
                        using (var transaction = connection.BeginTransaction(System.Data.IsolationLevel.ReadCommitted))
                        //using (var transaction = new TransactionScope(TransactionScopeOption.Required, options))
                        {
                            try
                            {
                                //LogManager.Debug(managerID, ipAddress, "[SaveWebOrder] Begin Transaction");
                                var orderPrefix = String.Format("SPWEB{0}", DateTime.Now.ToString("yyyyMMdd"));
                                var orderNumber = context.Orders.Count(item => item.OrderID.StartsWith(orderPrefix)) + 1;

                                var order = new Order()
                                    {
                                        OrderID = orderPrefix + orderNumber.ToString(),
                                        UserID = userId,
                                        DateTime = DateTime.UtcNow,
                                        TotalAmount = 0, // will be updated later
                                        TotalCommission = 0, // will be updated later
                                        IsReservation = true,
                                        PaymentType = paymentType.ToDatabase(),
                                        CCAuthorization = paymentAuthorization.Trim(),
                                        Status = "P", //O: Open; P: Paid; U: Used
                                        IPAddress = ipAddress
                                    };
                                //LogManager.Debug(managerID, ipAddress, "[SaveWebOrder] Order Initialized");
                                context.Orders.Add(order);
                                context.SaveChanges();
                                //LogManager.Debug(managerID, ipAddress, "[SaveWebOrder] Order Created");

                                var row = 1;
                                foreach (var detail in reservations)
                                {
                                    detail.OrderID = order.OrderID;
                                    detail.OrderDetailID = row++;
                                    context.OrderDetails.Add(detail);
                                    order.TotalAmount += detail.Amount.Value;
                                    order.TotalCommission += detail.Commission.Value;
                                    //context.SaveChanges();
                                    LogManager.Log(managerId, LogActions.INSERT, ipAddress, OrderDetailsTableName, detail.OrderDetailID.ToString());
                                }
                                LogManager.Log(managerId, LogActions.INSERT, ipAddress, OrderTableName, order.OrderID, String.Format("Monto: ¢{0} Comisión: ¢{1} Total: ¢{2} ${3} Fee: ${4}", order.TotalAmount, order.TotalCommission, order.TotalAmount + order.TotalCommission, paymentAmount, paymentFee));
                                //LogManager.Debug(managerID, ipAddress, "[SaveWebOrder] Order Details Saved");
                                
                                context.SaveChanges();
                                transaction.Commit();
                                //LogManager.Debug(managerID, ipAddress, "[SaveWebOrder] End Transaction");

                                return true;
                            }
                            catch
                            {
                                transaction.Rollback();
                                throw;
                            }
                        }
                    }
                }
                //LogManager.Debug(managerID, ipAddress, "[SaveWebOrder] After Transaction");
            }
            catch (TransactionAbortedException ex)
            {
                LogManager.Error(managerId, ipAddress, "TransactionAbortedException: " + ex.Message);
            }
            catch (ApplicationException ex)
            {
                LogManager.Error(managerId, ipAddress, "ApplicationException: " + ex.Message);
            }
            catch (Exception ex)
            {
                LogManager.Error(managerId, ipAddress, "Exception: " + ex.Message + " " + ex.InnerException.Message);
            }
            finally
            {
                //LogManager.Debug(managerID, ipAddress, "[SaveWebOrder] Exit");
            }
            return false;
        }

        public enum PaymentType
        {
            Cash,
            CreditCard,
            PayPal,
            SmartCash           
        }

        internal static string ToDatabase(this PaymentType paymentType)
        {
            switch (paymentType)
            {
                case PaymentType.Cash: return "C";
                case PaymentType.CreditCard: return "CC";
                case PaymentType.PayPal: return "PP";
                case PaymentType.SmartCash: return "SC";
                default: return "";
            }
        }

        internal static PaymentType FromDatabase(this string paymentType)
        {
            switch (paymentType.Trim())
            {
                case "C": return PaymentType.Cash;
                case "CC": return PaymentType.CreditCard;
                case "PP": return PaymentType.PayPal;
                case "SC": return PaymentType.SmartCash;
                default: return PaymentType.Cash;
            }
        }

        public static string GetPaymentTypeName(this string paymentType)
        {
            return GetPaymentTypeName(paymentType.FromDatabase());
        }

        public static string GetPaymentTypeName(this PaymentType paymentType)
        {
            switch (paymentType)
            {
                case PaymentType.Cash: return "Efectivo";
                case PaymentType.CreditCard: return "Tarjeta de Crédito";
                case PaymentType.PayPal: return "PayPal";
                case PaymentType.SmartCash: return "SmartCash";
                default: return "";
            }
        }
    }
}
